S01-09 JavaSE-面向对象-进阶
[TOC]
包
概述
多个程序员开发同一个项目时,可能出现类名冲突(如两个 Dog 类),通过包区分。
包(Package):是 Java 中管理类和接口的核心机制,本质是命名空间(Namespace),用于解决类名冲突、组织代码结构、控制访问权限三大核心问题。它是 Java 模块化编程的基础,贯穿于类的定义、导入、编译、运行全流程。每个类都属于一个包,若未显式声明包,则属于默认包(无名包)。
包的本质:命名空间与代码组织
- 命名空间:通过包名给类名添加前缀,避免不同包下的类名冲突(如
java.util.Date和java.sql.Date); - 代码组织:按功能模块划分包,使代码结构清晰,便于维护(如
com.demo.controller存放控制器、com.demo.service存放服务类)。
包的命名规则
Java 官方规定包名采用反向域名命名法,确保全球唯一:
- 只能包含数字/字母/下划线/点
.,不能以数字开头,不能有关键字/保留字; - 全部小写,避免大小写敏感问题;
- 以公司 / 组织的反向域名开头,后接功能模块;
- 不同层级用点
.分隔。
// 公司项目
package com.baidu.xxx
// 个人项目
package com.demo.controller
package com.demo.service基本语法
包的声明
包的声明语法格式:
package 包名;核心规则:
- 必须是源文件的第一条语句:在
package语句之前不能有任何代码(可以有注释); - 一个源文件只能有一个 package 语句:一个类只能属于一个包;
- 包名与目录结构一致:编译后,包名对应操作系统的目录结构(如
com.demo对应com/demo目录)。
示例:
// 正确:package 是第一条语句
package com.demo.model;
public class User {
private String name;
}默认包
若源文件中未显式声明 package 语句,则该类属于默认包(无名包)。
默认包特点:
- 包名为空,类名无前缀;
- 不同源文件的默认包类之间可直接访问(默认访问权限);
- 不推荐使用:易导致类名冲突,且无法导入其他包的类(JDK 1.4 后限制)。
示例:
// 无 package 语句,属于默认包
public class DefaultPackageClass {}快速入门
// 包com.xiaoming下的Dog类
package com.xiaoming;
public class Dog {}// 包com.xiaoqiang下的Dog类
package com.xiaoqiang;
public class Dog {}// 测试类
package com.use;
import com.xiaoming.Dog; // 导入指定包的类
public class Test {
public static void main(String[] args) {
Dog dog1 = new Dog(); // com.xiaoming.Dog
com.xiaoqiang.Dog dog2 = new com.xiaoqiang.Dog(); // 全类名访问
}
}三大核心作用
包的三大核心作用:
作用 1:解决类名冲突(核心作用)
不同包下的类可以同名,通过全限定类名(包名 + 类名)区分。
java// 1. 导入 java.util 包的 Date 类 import java.util.Date; // 2. 导入 java.sql 包的 Date 类,用别名区分 import java.sql.Date as SqlDate; public class DateDemo { public static void main(String[] args) { Date utilDate = new Date(); // java.util.Date java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis()); // 全限定类名 } }作用 2:组织代码结构(模块化编程)
按功能模块划分包,使代码结构清晰,符合 “高内聚、低耦合” 的设计原则。
plaintextcom.demo ├── controller // 控制器(处理请求) ├── service // 服务层(业务逻辑) ├── dao // 数据访问层(操作数据库) ├── model // 实体类(数据模型) ├── util // 工具类(通用功能) └── config // 配置类(全局配置)作用 3:控制访问权限(与访问修饰符配合)
包与访问修饰符(
default、protected)配合,实现类的访问控制:- default 访问权限(无修饰符):仅同包内的类可访问;
- protected 访问权限:同包内的类 + 不同包的子类可访问。
java// 包 com.demo.a package com.demo.a; // default 权限的类:仅同包可访问 class DefaultClass { public void show() { System.out.println("default 类的方法"); } } public class TestA { public static void main(String[] args) { // 同包的测试类:可访问 DefaultClass DefaultClass dc = new DefaultClass(); // 合法 dc.show(); } }java// 不同包的测试类:不可访问 DefaultClass package com.demo.b; import com.demo.a.DefaultClass; // 错误:DefaultClass 是 default 权限,无法导入 public class TestB { public static void main(String[] args) { // DefaultClass dc = new DefaultClass(); // 错误:无法访问 } }
类的导入
通过 import 语句引入其他包的类,避免在代码中重复写全限定类名,简化代码编写。
语法格式
语法格式:
格式 1:导入单个类
javaimport 包名.类名;格式 2:导入包下所有类(通配符
*)javaimport 包名.*;格式 3:静态导入(导入类的静态成员)
java// 导入单个静态成员 import static 包名.类名.静态成员名; // 导入所有静态成员 import static 包名.类名.*;
核心规则:
import 语句的位置:必须在
package语句之后,类定义之前;一个源文件可有多条 import 语句:按需导入多个包的类;
通配符
*仅导入当前包的类:不导入子包的类(如import com.demo.*不导入com.demo.controller.User);默认导入:JVM 自动导入
java.lang.*包下的所有类(如String、System、Object),无需显式导入。
示例:
单个类导入与全限定类名
javapackage com.demo.test; // 1. 导入 java.util 包的 ArrayList 类 import java.util.ArrayList; public class ImportDemo { public static void main(String[] args) { // 2. 简化写法:直接使用类名 ArrayList<String> list1 = new ArrayList<>(); // 全限定类名:不导入也可使用,但繁琐 java.util.HashMap<String, String> map = new java.util.HashMap<>(); } }通配符导入
javapackage com.demo.test; // 1. 导入 java.util 包下所有类 import java.util.*; public class ImportWildcardDemo { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); // 2. 合法 HashMap<String, String> map = new HashMap<>(); // 3. 合法 } }静态导入
javapackage com.demo.test; // 1. 静态导入 Math 类的 PI 常量和 sqrt 方法 import static java.lang.Math.PI; import static java.lang.Math.sqrt; public class StaticImportDemo { public static void main(String[] args) { // 2. 简化写法:直接使用静态成员 System.out.println(PI); // 3.141592653589793 System.out.println(sqrt(4)); // 2.0 // 3. 不静态导入的写法:Math.PI、Math.sqrt(4) } }
解决类名冲突
当导入的两个包中有同名类时,需通过全限定类名区分,或使用导入别名(JDK 10+ 支持)。
方式 1:全限定类名
package com.demo.test;
// 1. 导入 java.util.Date
import java.util.Date;
public class DateConflictDemo {
public static void main(String[] args) {
// 2. java.util.Date
Date utilDate = new Date();
// 3. 用全限定类名区分 java.sql.Date
java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());
}
}方式 2:导入别名
package com.demo.test;
// 给 java.sql.Date 起别名 SqlDate
import java.sql.Date as SqlDate;
import java.util.Date;
public class DateAliasDemo {
public static void main(String[] args) {
Date utilDate = new Date();
SqlDate sqlDate = new SqlDate(System.currentTimeMillis()); // 使用别名
}
}包的编译与运行
命令行
Java 编译器要求包名与目录结构完全一致,否则会编译失败或运行时找不到类。
命令行编译与运行步骤:
步骤 1:创建目录结构
假设包名是
com.demo,需创建目录com/demo,将Hello.java放入该目录:plaintext└── com └── demo └── Hello.java步骤 2:编写代码(Hello.java)
javapackage com.demo; public class Hello { public void sayHello() { System.out.println("Hello, Package!"); } }步骤 3:编译代码(javac 命令)
在根目录(
com目录的上级目录)执行编译命令,使用 :shjavac -d bin com/demo/Hello.java-d bin:指定编译后类文件的输出目录(保持包结构),如bin/com/demo/Hello.class。
步骤 4:运行代码(java 命令)
在根目录执行运行命令,使用全限定类名(包名 + 类名):
shjava -cp bin com.demo.Hello-cp bin:指定类路径为bin目录;com.demo.Hello:全限定类名,不能写Hello或com/demo/Hello。
IDE
IDEA、Eclipse 等 IDE 会自动根据 package 语句创建目录结构,编译和运行时自动处理类路径,无需手动操作。
IDEA 中创建包的步骤:
- 右键
src目录 →New→Package; - 输入包名(如
com.demo.controller),IDEA 自动创建com/demo/controller目录; - 在包下创建类,自动生成
package语句。
Java 核心包
JDK 提供了大量内置包,包含常用的类和接口,以下是最核心的几个:
java.lang:核心包,自动导入,包含
String、Object、System、Math等类;java.util:工具包,包含
ArrayList、HashMap、Date、Random、Scanner等类;java.io:输入输出包,包含文件操作、流操作相关类(如
File、InputStream);java.net:网络编程包,包含
Socket、ServerSocket等类;java.sql:数据库编程包,包含
Connection、Statement、ResultSet等接口;java.awt/swing:图形界面编程包,包含窗口、按钮等组件类。
访问修饰符
Java 提供 4 种访问修饰符(Modifier),其访问范围与包密切相关。以下是类和成员的访问权限对比表:
类的访问修饰符
类只能用 public 或 default(无修饰符)修饰,访问范围如下:
| 修饰符 | 同包可见 | 不同包可见 | 核心说明 |
|---|---|---|---|
public | ✅ | ✅ | 所有包可访问,一个 .java 文件只能有一个 public 类,且类名与文件名一致 |
default | ✅ | ❌ | 仅同包可访问,无修饰符的类属于默认访问权限 |
类成员的访问修饰符
类的成员(变量、方法、内部类)可使用 4 种修饰符,访问范围如下:
| 修饰符 | 本类 | 同包 | 不同包子类 | 所有包 | 核心说明 |
|---|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ | 仅本类可访问,与包无关 |
default | ✅ | ✅ | ❌ | ❌ | 仅同包可访问,无修饰符 |
protected | ✅ | ✅ | ✅ | ❌ | 同包 + 不同包子类可访问 |
public | ✅ | ✅ | ✅ | ✅ | 所有包可访问 |
示例:
示例 1:protected 跨包访问
protected修饰的成员,不同包的子类只能通过子类实例访问,不能通过父类实例访问。java// 父类:com.demo.parent package com.demo.parent; public class Parent { protected String name = "Parent"; }java// 子类:com.demo.child(不同包) package com.demo.child; import com.demo.parent.Parent; public class Child extends Parent { public void test() { // 1. ✅ 合法:子类实例访问 protected 成员 System.out.println(this.name); // Parent // 2. ✅ 合法:子类实例访问 Child child = new Child(); System.out.println(child.name); // 3. ❌ 错误:不同包不能通过父类实例访问 protected 成员 Parent parent = new Parent(); // System.out.println(parent.name); } }示例 2:四种访问修饰符的访问范围
javapackage com.hspedu.modifier; public class A { // 修饰属性 public int n1 = 100; protected int n2 = 200; int n3 = 300; private int n4 = 400; // 修饰方法 public void m1() {} protected void m2() {} void m3() {} private void m4() {} public void hi() { // 1. ✅ 同类可访问所有属性和方法 System.out.println(n1 + " " + n2 + " " + n3 + " " + n4); m1(); m2(); m3(); m4(); } }java// 同包: package com.hspedu.modifier; public class B { public void say() { A a = new A(); // 2. ✅ 同包可访问:public/protected/default属性和方法 System.out.println(a.n1 + " " + a.n2 + " " + a.n3); a.m1(); a.m2(); a.m3(); // 3. ❌ 同包类不可访问:private属性和方法 // System.out.println(a.n4); // a.m4(); } }java// 不同包: package com.hspedu.pkg; public class C { public void say() { A a = new A(); // 2. ✅ 不同包可访问:public属性和方法 System.out.println(a.n1); a.m1(); // 3. ❌ 不同包不可访问:protected/default/private属性和方法 // System.out.println(a.n2 + " " + a.n3 + "" + a.n4); // a.m2(); a.m3();a.m4(); } }
封装
OOP 三大特性
Java 面向对象编程的三大核心特性:封装、继承、多态
三大特性是 OOP 的基石,封装为 “基础”(保护数据、隐藏细节),继承为 “手段”(复用代码、构建层次),多态为 “延伸”(灵活扩展、解耦设计),三者相辅相成,共同构建健壮、可扩展的面向对象系统。
- 封装(Encapsulation):OOP 的基础,“隐藏细节,暴露接口”
- 继承(Inheritance):OOP 的手段,“复用代码,构建层次”
- 多态(Polymorphism):OOP 的延伸,“一个接口,多种实现”
封装概述
封装(Encapsulation):是指将对象的内部状态(属性)和行为(方法)捆绑在一起,隐藏内部实现细节,仅通过公开的接口与外部交互,从而实现 “信息隐藏” 和 “数据保护”。
本质:信息隐藏 + 数据保护
封装的核心是 “隐藏实现,暴露接口”:
- 信息隐藏:对外隐藏对象的内部状态和实现细节(如属性的存储方式、方法的执行逻辑),外部无法直接操作;
- 数据保护:通过可控的接口(getter/setter)操作数据,在接口中添加校验逻辑,保证数据的合法性和一致性。
封装的核心优势
封装的核心优势:
数据安全:
通过 setter 中的校验逻辑,杜绝非法数据(如年龄负数、姓名为空),保证对象状态的一致性。
降低耦合:
外部仅依赖公开接口,不依赖内部实现。例如:修改
Person类的age属性名为userAge,只需修改getAge()/setAge()的内部实现,外部调用处无需任何修改。代码可维护:
所有数据访问逻辑集中在 getter/setter 中,修改时只需改一处。例如:将年龄的校验规则从
0-150改为0-120,仅需修改setAge()方法。增强可扩展性:
可在接口中灵活添加额外逻辑,不影响外部调用:
java// 扩展:在setter中添加日志 public void setAge(int age) { if (age < 0 || age > 150) { throw new IllegalArgumentException("年龄必须在0-150之间!"); } // 新增日志逻辑,外部调用无感知 System.out.println("修改年龄:" + this.age + " → " + age); this.age = age; }提升代码复用性:
内部私有方法可被多个公共接口复用,避免代码冗余。例如:
calculateDiscount()可被createOrder()、refundOrder()等方法复用。
封装的实现步骤
封装的实现步骤:
Java 中封装的实现依赖访问修饰符(主要是 private)和公共接口(getter/setter),核心步骤如下:
步骤 1:私有化成员变量(核心)
将类的成员变量用
private修饰,禁止外部类直接访问,这是封装的基础。步骤 2:提供公共的 getter/setter 方法
IDEA 快捷键:
Alt + Insert- getter 方法:用于获取成员变量的值,命名规则
getXxx()(布尔类型可简化为isXxx()); - setter 方法:用于修改成员变量的值,命名规则
setXxx(参数); - 方法用
public修饰,作为外部访问私有变量的唯一入口。
- getter 方法:用于获取成员变量的值,命名规则
步骤 3:在 setter 中添加数据校验(可选但推荐)
在 setter 方法中校验传入值的合法性,拒绝非法数据(如年龄 < 0、姓名为空),保证数据一致性。
步骤 4:在 getter 中添加权限判断(可选)
步骤 5:隐藏内部业务逻辑(进阶)
将内部辅助方法(如计算、校验的工具方法)私有化,仅暴露对外的核心业务方法。
快速入门
对比未封装 vs 封装:
未封装的 Person 类(问题暴露)
成员变量用
public修饰,外部可直接访问和修改,导致数据失控:java// 未封装的类:数据不安全,耦合度高 public class UnEncapsulatedPerson { // 公共成员变量:外部可直接修改 public String name; public int age; public static void main(String[] args) { UnEncapsulatedPerson person = new UnEncapsulatedPerson(); // 问题1:可随意设置非法数据(年龄为负数) person.age = -20; // 问题2:可设置空姓名 person.name = ""; // 问题3:后续若修改属性名(如age改为userAge),所有外部调用处都要改 System.out.println("姓名:" + person.name + ",年龄:" + person.age); // 姓名:,年龄:-20 } }封装后的 Person 类(最佳实践)
私有化成员变量,提供带校验的 getter/setter,隐藏内部逻辑:
java// 封装后的类:数据安全,可维护性高 public class EncapsulatedPerson { // 1. 私有化成员变量:外部无法直接访问 private String name; private int age; // 2. 提供 getter 方法:获取属性值 public String getName() { return this.name; }// 3. 提供 setter 方法:修改属性值,添加数据校验 public void setName(String name) { // 校验:姓名不能为空或空白字符串 if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("姓名不能为空!"); } this.name = name; } public int getAge() { return this.age; } public void setAge(int age) { // 校验:年龄必须在0-150之间 if (age < 0 || age > 150) { throw new IllegalArgumentException("年龄必须在0-150之间!"); } this.age = age; } // 4. 隐藏内部业务逻辑:私有化辅助方法 private void validatePerson() { System.out.println("校验人员信息:" + this.name + "," + this.age); } // 5. 暴露公共业务接口:外部仅能调用该方法 public void showPersonInfo() { this.validatePerson(); // 内部调用私有方法 System.out.println("姓名:" + this.name + ",年龄:" + this.age); } // 测试封装效果 public static void main(String[] args) { EncapsulatedPerson person = new EncapsulatedPerson(); // 合法数据:正常设置 person.setName("张三"); person.setAge(25); person.showPersonInfo(); // 校验人员信息:张三,25 → 姓名:张三,年龄:25 // 非法数据:抛出异常,保证数据安全 try { person.setName(""); // 抛出:IllegalArgumentException: 姓名不能为空! } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } try { person.setAge(-5); // 抛出:IllegalArgumentException: 年龄必须在0-150之间! } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } } } 关键对比:封装前后的差异
维度 未封装(public 变量) 封装(private 变量 + getter/setter) 数据安全 外部可随意设置非法值 仅能通过校验后的 setter 设置合法值 代码耦合 外部直接依赖属性名,修改属性名需改所有调用处 外部依赖接口,修改属性名仅需改 getter/setter 逻辑扩展 无法添加额外逻辑(如日志、校验) 可在 getter/setter 中添加日志、缓存等 可维护性 低(校验逻辑分散在各处) 高(校验逻辑集中在 setter)
访问修饰符
封装的实现依赖 Java 的访问修饰符,通过不同的修饰符控制类 / 成员的可见范围,核心修饰符对比如下:
| 修饰符 | 本类 | 同包 | 不同包子类 | 所有类 | 封装场景中的作用 |
|---|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ | 私有化成员变量 / 内部方法,核心封装修饰符 |
default | ✅ | ✅ | ❌ | ❌ | 同包内可见,用于包内封装 |
protected | ✅ | ✅ | ✅ | ❌ | 子类可见,用于继承场景的封装 |
public | ✅ | ✅ | ✅ | ✅ | 暴露公共接口(getter/setter/ 业务方法) |
关键说明:
- 封装的核心是
private:90% 的封装场景都是将成员变量设为private,仅暴露public接口; default用于包内封装:仅同包内的类可访问,适合包内复用的工具方法;protected用于继承封装:允许子类访问,但禁止外部无关类访问;public仅暴露必要接口:避免将所有方法设为public,遵循 “最小权限原则”。
构造器结合 setter
虽然在类中设置了 getter/setter,但是在初始化对象时会调用构造器,默认情况下可以通过构造器绕过设置的 getter/setter 方法。此时就需要通过在构造器中调用 setter 方法确保初始化时也能进行数据验证。
public Person (String name, Int age) {
// 在构造器中调用 setter 方法
this.setName(name);
this.setAge(age);
}封装进阶场景
只读/只写属性
只读 / 只写属性:按需暴露接口
封装并非必须同时提供 getter 和 setter,可根据需求仅暴露部分接口:
- 只读属性:只提供 getter,不提供 setter(如用户 ID,创建后不可修改);
- 只写属性:只提供 setter,不提供 getter(如密码,仅能设置,不能获取)。
public class User {
// 只读属性:用户ID(创建后不可改)
private final String userId;
// 只写属性:密码(仅能设置,不能获取)
private String password;
// 构造方法初始化只读属性
public User(String userId) {
this.userId = userId;
}
// 只读:仅提供getter
public String getUserId() {
return this.userId;
}
// 只写:仅提供setter,且添加密码强度校验
public void setPassword(String password) {
if (password == null || password.length() < 6) {
throw new IllegalArgumentException("密码长度不能小于6位!");
}
this.password = password;
}
}隐藏复杂业务逻辑
隐藏复杂业务逻辑:
将复杂的内部逻辑私有化,仅暴露简单的公共接口,降低外部使用成本:
public class OrderService {
// 私有化内部方法:计算折扣
private double calculateDiscount(double amount, int vipLevel) {
if (vipLevel == 1) return amount * 0.9;
if (vipLevel == 2) return amount * 0.8;
return amount;
}
// 私有化内部方法:生成订单号
private String generateOrderId() {
return "ORDER_" + System.currentTimeMillis();
}
// 暴露公共接口:创建订单(外部仅需调用该方法)
public String createOrder(double amount, int vipLevel) {
double finalAmount = this.calculateDiscount(amount, vipLevel);
String orderId = this.generateOrderId();
System.out.println("创建订单:" + orderId + ",金额:" + finalAmount);
return orderId;
}
}
// 外部调用:无需关心折扣计算、订单号生成的细节
class TestOrder {
public static void main(String[] args) {
OrderService service = new OrderService();
service.createOrder(1000, 2); // 创建订单:ORDER_17362xxxx,金额:800.0
}
}不可变类(终极封装)
不可变类:终极封装
不可变类是封装的极致形式:对象创建后,属性不可修改,核心特点:
- 成员变量私有化 +
final修饰; - 无 setter 方法;
- 构造方法初始化所有成员;
- 类用
final修饰(禁止继承,避免子类修改逻辑)。
Java 中的 String、Integer 等包装类都是不可变类:
// 自定义不可变类:地址类
public final class Address {
// 私有 + final 成员变量
private final String province;
private final String city;
// 构造方法初始化所有成员
public Address(String province, String city) {
this.province = province;
this.city = city;
}
// 仅提供 getter,无 setter
public String getProvince() {
return province;
}
public String getCity() {
return city;
}
}
// 测试不可变类:属性无法修改
class TestAddress {
public static void main(String[] args) {
Address addr = new Address("广东省", "深圳市");
System.out.println(addr.getProvince()); // 广东省
// 无 setter,无法修改属性,保证对象不可变
}
}练习
需求:创建 Account 类,要求:
- 姓名:长度 2-4 位,否则默认"无名"
- 余额:必须>20,否则默认 0
- 密码:必须 6 位,否则默认"000000"
- 提供 setter/getter 方法和
showInfo()方法
package com.hspedu.encap;
public class Account {
// 私有化属性
private String name;
private double balance;
private String pwd;
// 构造器
public Account() {}
public Account(String name, double balance, String pwd) {
this.setName(name);
this.setBalance(balance);
this.setPwd(pwd);
}
// getter 方法
public String getName() {
return name;
}
public double getBalance() {
return balance;
}
public String getPwd() {
return pwd;
}
// setter 方法(含验证)
public void setName(String name) {
if (name.length() >= 2 && name.length() <= 4) {
this.name = name;
} else {
System.out.println("姓名要求(长度为2-4位),默认值无名");
this.name = "无名";
}
}
public void setBalance(double balance) {
if (balance > 20) {
this.balance = balance;
} else {
System.out.println("余额(必须>20),默认为0");
this.balance = 0;
}
}
public void setPwd(String pwd) {
if (pwd.length() == 6) {
this.pwd = pwd;
} else {
System.out.println("密码(必须是六位),默认密码为000000");
this.pwd = "000000";
}
}
// 显示账号信息
public void showInfo() {
// 可以添加权限校验
System.out.println("账号信息:name=" + name + " 余额=" + balance + " 密码=" + pwd);
}
}// 测试类
package com.hspedu.encap;
public class TestAccount {
public static void main(String[] args) {
Account account = new Account();
account.setName("jack");
account.setBalance(60);
account.setPwd("123456");
account.showInfo(); // 账号信息:name=jack 余额=60.0 密码=123456
}
}继承
概述
多个类存在相同的属性和方法时,代码冗余,维护成本高。通过继承抽取父类,实现代码复用。
Pupil(小学生)和 Graduate(大学生)都有 name、age、score 属性和 testing()、showInfo()方法,可抽取父类 Student。
继承(Inheritance):是基于已有的类(父类 / 超类)创建新类(子类 / 派生类),子类复用父类的属性和方法,同时可扩展自身的功能。它是实现代码复用、构建类层次结构、支撑多态的核心机制。
本质:代码复用 + 类层次化
- 代码复用:避免重复编写相同的属性和方法(如多个子类都需要 “姓名、年龄” 属性,可抽离到父类);
- 类层次化:构建 “通用 → 具体” 的类结构(如
Animal→Mammal→Dog),符合现实世界的分类逻辑; - 多态基础:继承是多态的前提(子类对象可赋值给父类引用)。
继承关系示意图:

基本语法
基本语法:
使用 extends 关键字声明继承关系,格式如下:
// 父类(超类/基类)
public class 父类名 {
// 属性、方法
}
// 子类(派生类):继承父类
public class 子类名 extends 父类名 {
// 新增属性、方法,或重写父类方法
}核心规则:
规则 1:Java 是单继承
子类只能直接继承一个父类(避免多继承的菱形问题),但可通过 “多层继承” 间接继承多个类的特性:
java// 合法:多层继承 class A {} class B extends A {} class C extends B {} // C 间接继承 A // 错误:多继承(Java 不支持) // class D extends A, B {}规则 2:所有类默认继承 Object 类
Java 中没有显式声明父类的类,默认继承
java.lang.Object类(Object 是所有类的根父类)。Object 类提供了所有对象的通用方法(如
equals()、hashCode()、toString()),所有类都可直接使用或重写这些方法。java// 等价于 class Person extends Object {} public class Person {}规则 3:访问权限决定继承可见性
子类继承了父类的所有属性和方法,但
private成员因访问权限限制无法直接访问:javaclass Parent { public String pubVar = "公共变量"; protected String proVar = "保护变量"; String defVar = "默认变量"; private String priVar = "私有变量"; // 子类无法继承 } class Child extends Parent { public void show() { System.out.println(pubVar); // ✅ 合法:继承public成员 System.out.println(proVar); // ✅ 合法:继承protected成员 System.out.println(defVar); // ✅ 合法:同包下继承default成员 // System.out.println(priVar); // ❌ 错误:无法访问private成员 } }如果子类要访问父类的
private成员,需要通过父类的 非私有方法 访问:javaclass Parent { private String priVar = "私有变量"; // 子类无法继承 // 1. 通过非私有方法(public/protected/default)访问私有属性 public String getPriVar() { return this.priVar; } } class Child extends Parent { public void show() { getPriVar() // 2. 子类访问 } }
快速入门
// 父类:Student
public class Student {
// 共有属性
public String name;
public int age;
private double score; // 私有属性
// 共有方法
public void setScore(double score) {
this.score = score;
}
public void showInfo() {
System.out.println("学生名" + name + " 年龄" + age + " 成绩" + score);
}
}// 子类:Pupil(小学生)
public class Pupil extends Student {
// 特有方法
public void testing() {
System.out.println("小学生" + name + " 正在考小学数学..");
}
}// 子类:Graduate(大学生)
public class Graduate extends Student {
// 特有方法
public void testing() {
System.out.println("大学生" + name + " 正在考大学数学..");
}
}成员变量的继承
成员变量的继承规则:
子类继承父类非私有变量
子类可直接使用父类的
public/protected/default成员变量:javaclass Animal { protected String name; // 父类保护变量 protected int age; } class Dog extends Animal { public void setInfo(String name, int age) { this.name = name; // 直接使用父类的 name this.age = age; // 直接使用父类的 age } }子类同名变量隐藏父类变量
子类声明与父类同名的变量时,会 “隐藏” 父类的变量(而非覆盖),可通过
super关键字访问父类变量:javaclass Parent { String name = "父类姓名"; } class Child extends Parent { String name = "子类姓名"; // 隐藏父类 name public void show() { System.out.println(name); // 子类姓名(就近原则) System.out.println(super.name); // 父类姓名(通过super访问) } }
方法的继承
方法的继承:
子类可直接调用父类的非私有方法:
class Animal {
public void eat() {
System.out.println("动物进食");
}
}
class Cat extends Animal {
public void play() {
this.eat(); // 调用继承的父类方法
System.out.println("猫玩球");
}
}方法重写
概述
方法重写(Override):子类定义与父类 “方法签名完全一致” 的方法,覆盖父类的原有逻辑。
方法签名:方法名 + 参数列表(参数个数、类型、顺序),返回值需兼容(Java 5+ 支持协变返回)。
方法重写规则
方法重写的核心规则:必须遵守,否则编译报错
- 方法签名一致:方法名、参数列表(个数 / 类型 / 顺序)必须完全相同
- 返回值兼容:子类返回值需是父类返回值的子类(或相同)(协变返回)
- 访问权限不降级:子类方法的访问权限不能比父类更严格(如父类 public → 子类不能 private)
- 异常不扩大:子类方法抛出的异常不能比父类更广泛(如父类抛 RuntimeException → 子类不能抛 Exception)
示例:正确 vs 错误
// 父类
class Parent {
public Object show(String msg) throws RuntimeException {
System.out.println("父类方法:" + msg);
return msg;
}
}// ✅ 子类:正确重写
class Child1 extends Parent {
// 协变返回:Object → String(String是Object的子类)
@Override // 注解:强制检查重写规则,推荐添加
public String show(String msg) { // 父类抛RuntimeException,子类可不抛
System.out.println("子类方法:" + msg);
return msg;
}
}// ❌ 子类:错误重写(访问权限降级)
class Child2 extends Parent {
// @Override // 编译报错:父类是public,子类是private
private Object show(String msg) {
return null;
}
}@Override
@Override 注解的作用:
- 强制编译器检查是否符合重写规则,避免拼写错误(如把
show写成shwo); - 增强代码可读性,明确标识该方法是重写父类的方法;
- 推荐所有重写方法都添加该注解。
构造方法的继承
构造方法的继承:
构造方法不能被继承(构造方法名必须与类名一致,子类类名不同),但子类构造方法可通过 super() 调用父类构造方法。
核心规则:
子类构造方法默认调用父类的无参构造方法(
super()),且super()必须是构造方法的第一条语句;javaclass Parent { public Parent() { // 无参构造器 System.out.println("父类的无参构造器默认会被调用"); } } class Child extends Parent { public Child() { // super() 子类构造方法默认调用父类的无参构造方法 } }若父类没有无参构造(仅含带参构造),子类构造方法必须显式调用父类的带参构造(
super(参数));java// 父类:仅含带参构造(无默认无参构造) class Parent { private String name; public Parent(String name) { // 带参构造 this.name = name; } }java// 子类:必须显式调用父类带参构造 class Child extends Parent { private int age; // ✅ 正确:显式调用父类带参构造 public Child(String name, int age) { super(name); // 必须是第一条语句 this.age = age; } // ❌ 错误:未调用父类构造(父类无无参构造) // public Child() {} // 编译报错 }不能同时调用
super()和this()(二者都需是第一条语句)。
super
概述
super:访问父类成员
super 是继承场景的核心关键字,作用是在子类中访问父类的成员(变量 / 方法 / 构造),与 this 对应:
this指向当前对象。super指向父类对象。
核心用法
super 的核心用法:
super.成员变量:访问父类的非私有成员变量(解决子类同名变量的隐藏问题)super.方法名(参数):调用父类的非私有原方法(子类重写后,仍可调用父类原方法)super(参数):调用父类的构造方法(必须是构造方法第一条语句)
示例:super 的使用
// 父类
class Parent {
String name = "父类";
public Parent(String name) {
this.name = name;
}
public void show() {
System.out.println("父类show方法:" + name);
}
}// 子类
class Child extends Parent {
String name = "子类";
public Child(String parentName, String childName) {
super(parentName); // 3. 调用父类带参构造
this.name = childName;
}
@Override
public void show() {
super.show(); // 2. 调用父类的show方法
System.out.println("子类show方法:" + this.name);
System.out.println("父类name:" + super.name); // 1. 访问父类name
}
}// 测试
public class TestSuper {
public static void main(String[] args) {
Child child = new Child("父类姓名", "子类姓名");
child.show();
// 输出:
// 父类show方法:父类姓名
// 子类show方法:子类姓名
// 父类name:父类姓名
}
}继承的访问修饰符
不同访问修饰符的成员在继承中的可见性,是继承的核心考点,以下是完整对比表:
| 父类成员修饰符 | 同包子类可见 | 不同包子类可见 | 非子类可见 | 子类能否继承 |
|---|---|---|---|---|
public | ✅ | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ❌ | ✅ |
default | ✅ | ❌ | ❌ | ✅(同包) |
private | ❌ | ❌ | ❌ | ❌ |
关键说明:
protected是为继承设计的修饰符:不同包的子类可访问,但非子类不可访问;default仅同包的子类可继承,不同包的子类无法访问;private成员虽不能继承,但子类可通过父类的public/protected方法间接访问
继承的内存流程图
核心逻辑:子类对象创建后,会建立属性查找关系:
- 先查找子类是否有该属性,有则访问
- 若无,查找父类,有则访问(需满足访问权限)
- 若无,继续向上查找,直到 Object 类
- 注意:如果在查找的过程中遇到 private 属性,则不会在继续向上查找(即使上面有该属性并且可以访问),而是报错
// 爷类
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
// 父类
class Father extends GrandPa {
String name = "大头爸爸";
private int age = 39; // 私有属性
public int getAge() {
return age;
}
}
// 子类
class Son extends Father {
String name = "大头儿子";
}
// 测试类
public class Test {
public static void main(String[] args) {
Son son = new Son();
System.out.println(son.name); // 大头儿子(子类有name属性)
System.out.println(son.hobby); // 旅游(子类无,父类无,爷类有)
System.out.println(son.getAge()); // 39(父类私有属性,通过公共方法访问)
}
}内存流程图:
.jpg)
练习
练习 1
javaclass A { A() { System.out.println("a"); } A(String name) { System.out.println("a name"); } } class B extends A { B() { this("abc"); System.out.println("b"); } B(String name) { System.out.println("b name"); } // 有一个默认的 super(); } // main中:B b = new B(); 输出什么? // 输出结果:a → b name → b练习 2
javaclass A { public A() { System.out.println("我是A类"); } } class B extends A { public B() { System.out.println("我是B类的无参构造"); } public B(String name) { System.out.println(name + "我是B类的有参构造"); } } class C extends B { public C() { this("hello"); System.out.println("我是c类的无参构造"); } public C(String name) { super("hahah"); System.out.println("我是c类的有参构造"); } } public class ExtendsExercise02 { public static void main(String[] args) { C c = new C(); // 输出:我是A类 → hahah我是B类的有参构造 → 我是c类的有参构造 → 我是c类的无参构造 } }练习 3
需求:
- 编写 Computer 类:属性 CPU、内存、硬盘;方法
getDetails()返回详细信息 - 编写 PC 子类:继承 Computer,添加特有属性 brand(品牌)
- 编写 NotePad 子类:继承 Computer,添加特有属性 color(颜色)
- 测试类:创建 PC 和 NotePad 对象,赋值并打印信息
java// 父类:Computer public class Computer { private String cpu; private int memory; private int disk; public Computer(String cpu, int memory, int disk) { this.cpu = cpu; this.memory = memory; this.disk = disk; } public String getDetails() { return "cpu=" + cpu + " memory=" + memory + " disk=" + disk; } // getter/setter public String getCpu() { return cpu; } public void setCpu(String cpu) { this.cpu = cpu; } public int getMemory() { return memory; } public void setMemory(int memory) { this.memory = memory; } public int getDisk() { return disk; } public void setDisk(int disk) { this.disk = disk; } } // 子类:PC public class PC extends Computer { private String brand; public PC(String cpu, int memory, int disk, String brand) { super(cpu, memory, disk); // 调用父类构造器 this.brand = brand; } public void printInfo() { System.out.println("PC信息=" + getDetails() + " brand=" + brand); } // getter/setter public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } } // 测试类 public class ExtendsExercise03 { public static void main(String[] args) { PC pc = new PC("intel", 16, 500, "IBM"); pc.printInfo(); // PC信息=cpu=intel memory=16 disk=500 brand=IBM } }- 编写 Computer 类:属性 CPU、内存、硬盘;方法
super 关键字
基本介绍
super代表父类的引用,用于访问父类的属性、方法、构造器。
基本语法
- 访问父类属性:
super.属性名;(不能访问父类 private 属性) - 访问父类方法:
super.方法名(参数列表);(不能访问父类 private 方法) - 访问父类构造器:
super(参数列表);(只能在子类构造器中使用,且在第一行)
super 的使用细节
- 当子类和父类有同名成员时,用
super访问父类成员;无同名时,super可省略 super的访问不限于直接父类,若爷爷类有同名成员,也可通过super访问(遵循就近原则)- 调用父类构造器的好处:分工明确(父类属性由父类初始化,子类属性由子类初始化)
示例:
package com.hspedu.super_;
// 爷爷类
public class Base {
public int n1 = 999;
public void cal() {
System.out.println("Base类的cal()方法...");
}
}
// 父类
public class A extends Base {
protected int n2 = 200;
int n3 = 300;
private int n4 = 400;
public void cal() {
System.out.println("A类的cal()方法...");
}
}
// 子类
public class B extends A {
public int n1 = 888; // 与爷爷类同名
public void test() {
System.out.println("super.n1=" + super.n1); // 999(访问爷爷类的n1)
System.out.println("super.n2=" + super.n2); // 200(访问父类的n2)
super.cal(); // 调用父类的cal()方法 → A类的cal()方法...
}
}
// 测试类
public class Super01 {
public static void main(String[] args) {
B b = new B();
b.test();
}
}super 和 this 的比较
| 区别点 | this | super |
|---|---|---|
| 访问属性 | 先访问本类,无则父类 | 直接访问父类 |
| 调用方法 | 先访问本类,无则父类 | 直接访问父类 |
| 调用构造器 | 调用本类构造器(必须在第一行) | 调用父类构造器(必须在第一行) |
| 特殊含义 | 代表当前对象 | 代表父类对象的引用 |
方法重写/覆盖(override)
基本介绍
方法重写(override):子类有一个方法,与父类的方法名称、参数列表、返回类型完全一致(或子类返回类型是父类返回类型的子类),则子类方法重写了父类方法。
快速入门
package com.hspedu.override_;
// 父类
public class Animal {
public void cry() {
System.out.println("动物叫唤..");
}
public Object m1() {
return null;
}
}
// 子类
public class Dog extends Animal {
// 重写父类的cry()方法
@Override // 注解:标记方法重写(可选,推荐)
public void cry() {
System.out.println("小狗汪汪叫..");
}
// 重写父类的m1()方法(返回类型是父类返回类型的子类)
@Override
public String m1() {
return null;
}
}
// 测试类
public class Override01 {
public static void main(String[] args) {
Dog dog = new Dog();
dog.cry(); // 小狗汪汪叫..(调用子类重写的方法)
}
}注意事项和使用细节
- 方法名、参数列表必须与父类完全一致
- 返回类型:
- 子类返回类型与父类一致
- 或子类返回类型是父类返回类型的子类(如父类返回 Object,子类返回 String)
- 访问权限:子类方法不能缩小父类方法的访问权限(
public > protected > 默认 > private) - 父类 private 方法不能被重写(子类无法访问)
- 重写方法不能抛出比父类更多的异常(后续讲解)
课堂练习
练习 1:方法重写 vs 方法重载
| 名称 | 发生范围 | 方法名 | 形参列表 | 返回类型 | 修饰符 |
|---|---|---|---|---|---|
| 重载(overload) | 本类 | 必须相同 | 类型/个数/顺序至少一个不同 | 无要求 | 无要求 |
| 重写(override) | 父子类 | 必须相同 | 完全相同 | 相同或子类 | 子类不能缩小访问范围 |
练习 2
需求:
- Person 类:属性 name、age(private);构造器;
say()方法返回自我介绍 - Student 类:继承 Person,添加属性 id、score(private);构造器;重写
say()方法
package com.hspedu.override_;
// 父类:Person
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String say() {
return "name=" + name + " age=" + age;
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
// 子类:Student
public class Student extends Person {
private int id;
private double score;
public Student(String name, int age, int id, double score) {
super(name, age); // 调用父类构造器
this.id = id;
this.score = score;
}
// 重写say()方法
@Override
public String say() {
// 复用父类的say()方法,添加子类属性
return super.say() + " id=" + id + " score=" + score;
}
// getter/setter
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public double getScore() { return score; }
public void setScore(double score) { this.score = score; }
}
// 测试类
public class OverrideExercise {
public static void main(String[] args) {
Person jack = new Person("jack", 10);
System.out.println(jack.say()); // name=jack age=10
Student smith = new Student("smith", 20, 123456, 99.8);
System.out.println(smith.say()); // name=smith age=20 id=123456 score=99.8
}
}多态
问题引入
需求:Master 类的 feed()方法实现主人给动物喂食物(Dog 吃 Bone、Cat 吃 Fish、Pig 吃 Rice)。
传统方法问题:每增加一种动物/食物,需新增 feed()方法,代码冗余,维护性差。
解决方案:使用多态统一管理。
多态基本介绍
多态(polymorphism):方法或对象具有多种形态,建立在封装和继承基础之上。
多态的具体体现
1) 方法的多态(重载和重写)
package com.hspedu.poly_;
public class PloyMethod {
public static void main(String[] args) {
// 方法重载体现多态(参数不同,调用不同方法)
A a = new A();
System.out.println(a.sum(10, 20)); // 30
System.out.println(a.sum(10, 20, 30)); // 60
// 方法重写体现多态(子类重写父类方法)
B b = new B();
a.say(); // A say() 方法被调用...
b.say(); // B say() 方法被调用...
}
}
class B { // 父类
public void say() {
System.out.println("B say() 方法被调用...");
}
}
class A extends B { // 子类
// 方法重载
public int sum(int n1, int n2) {
return n1 + n2;
}
public int sum(int n1, int n2, int n3) {
return n1 + n2 + n3;
}
// 方法重写
@Override
public void say() {
System.out.println("A say() 方法被调用...");
}
}2) 对象的多态(核心)
核心结论:
- 一个对象的编译类型和运行类型可以不一致
- 编译类型:定义对象时确定(=左边),不能改变
- 运行类型:创建对象时确定(=右边),可以改变
示例:
package com.hspedu.poly_.objectpoly_;
// 父类
public class Animal {
public void cry() {
System.out.println("动物在叫....");
}
}
// 子类:Dog
public class Dog extends Animal {
@Override
public void cry() {
System.out.println("小狗汪汪叫...");
}
}
// 子类:Cat
public class Cat extends Animal {
@Override
public void cry() {
System.out.println("小猫喵喵叫...");
}
}
// 测试类
public class PolyObject {
public static void main(String[] args) {
// 编译类型Animal,运行类型Dog
Animal animal = new Dog();
animal.cry(); // 小狗汪汪叫...(调用运行类型的方法)
// 编译类型不变,运行类型改为Cat
animal = new Cat();
animal.cry(); // 小猫喵喵叫...(调用新的运行类型的方法)
}
}多态快速入门案例(主人喂食物)
package com.hspedu.poly_;
// 食物类(父类)
public class Food {
private String name;
public Food(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// 动物类(父类)
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// 子类:Dog
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
}
// 子类:Bone
public class Bone extends Food {
public Bone(String name) {
super(name);
}
}
// 主人类
public class Master {
private String name;
public Master(String name) {
this.name = name;
}
// 多态方法:参数为父类类型,可接收子类对象
public void feed(Animal animal, Food food) {
System.out.println("主人" + name + " 给" + animal.getName() + " 吃" + food.getName());
}
}
// 测试类
public class Poly01 {
public static void main(String[] args) {
Master master = new Master("韩顺平");
Animal dog = new Dog("大黄");
Food bone = new Bone("大骨头");
master.feed(dog, bone); // 主人韩顺平 给大黄 吃大骨头
// 新增Pig和Rice,无需修改feed()方法
Animal pig = new Pig("小花");
Food rice = new Rice("白米饭");
master.feed(pig, rice); // 主人韩顺平 给小花 吃白米饭
}
}多态注意事项和细节
1) 多态的前提
- 两个类存在继承关系
- 子类重写父类方法
2) 多态向上转型(自动转换)
- 语法:
父类类型 引用名 = new 子类类型(); - 特点:
- 编译类型看左边,运行类型看右边
- 可调用父类所有成员(需满足访问权限)
- 不能调用子类特有成员
3) 多态向下转型(强制转换)
- 语法:
子类类型 引用名 = (子类类型) 父类引用; - 要求:
- 父类引用必须指向当前子类类型的对象(否则报 ClassCastException)
- 向下转型后,可调用子类特有成员
4) 属性没有重写之说
- 属性的值看编译类型(与方法不同)
5) instanceof 关键字
- 作用:判断对象的运行类型是否为指定类型或其子类型
- 语法:
对象 instanceof 类型→ 返回 boolean
示例代码:
package com.hspedu.poly_.detail_;
public class PolyDetail {
public static void main(String[] args) {
// 向上转型
Animal animal = new Cat();
animal.eat(); // 猫吃鱼(子类重写的方法)
// animal.catchMouse(); // 错误:不能调用子类特有方法
// 向下转型(需先判断类型)
if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.catchMouse(); // 猫抓老鼠(子类特有方法)
}
// 属性看编译类型
System.out.println(animal.name); // 动物(父类属性)
}
}
class Animal {
String name = "动物";
public void eat() {
System.out.println("吃");
}
}
class Cat extends Animal {
String name = "小猫";
@Override
public void eat() {
System.out.println("猫吃鱼");
}
// 子类特有方法
public void catchMouse() {
System.out.println("猫抓老鼠");
}
}课堂练习
练习 1
判断以下代码正确性:
public class PolyExercise01 {
public static void main(String[] args) {
double d = 13.4;
long l = (long) d; // 正确:强制类型转换
System.out.println(l); // 13
int in = 5;
boolean b = (boolean) in; // 错误:boolean与int不能相互转换
Object obj = "Hello";
String objStr = (String) obj; // 正确:向下转型(obj运行类型是String)
System.out.println(objStr); // Hello
Object objPri = new Integer(5);
String str = (String) objPri; // 错误:ClassCastException(运行类型是Integer)
Integer str1 = (Integer) objPri; // 正确:向下转型
}
}Java 的动态绑定机制(重要)
动态绑定机制:
- 当调用对象方法时,方法会与对象的运行类型绑定(而非编译类型)
- 当调用对象属性时,没有动态绑定机制,直接访问编译类型的属性
示例:
package com.hspedu.poly_.dynamic_;
public class DynamicBinding {
public static void main(String[] args) {
A a = new B(); // 编译类型A,运行类型B
System.out.println(a.sum()); // 40 → 解析:B的getI()返回20,20+20=40
System.out.println(a.sum1()); // 30 → 解析:A的sum1()访问A的i=10,10+20=30
}
}
class A { // 父类
public int i = 10;
public int sum() {
return getI() + 20;
}
public int sum1() {
return i + 20;
}
public int getI() {
return i;
}
}
class B extends A { // 子类
public int i = 20;
@Override
public int getI() {
return i;
}
}多态的应用
1) 多态数组
- 数组定义类型为父类类型,存储子类对象
- 访问子类特有方法需向下转型
示例:
package com.hspedu.poly_.polyarr_;
// 父类:Person
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String say() {
return name + "\t" + age;
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
// 子类:Student
public class Student extends Person {
private double score;
public Student(String name, int age, double score) {
super(name, age);
this.score = score;
}
@Override
public String say() {
return "学生" + super.say() + " score=" + score;
}
// 特有方法
public void study() {
System.out.println("学生" + getName() + " 正在学java...");
}
}
// 子类:Teacher
public class Teacher extends Person {
private double salary;
public Teacher(String name, int age, double salary) {
super(name, age);
this.salary = salary;
}
@Override
public String say() {
return "老师" + super.say() + " salary=" + salary;
}
// 特有方法
public void teach() {
System.out.println("老师" + getName() + " 正在讲java课程...");
}
}
// 测试类
public class PloyArray {
public static void main(String[] args) {
// 多态数组:存储Person、Student、Teacher对象
Person[] persons = new Person[5];
persons[0] = new Person("jack", 20);
persons[1] = new Student("mary", 18, 100);
persons[2] = new Student("smith", 19, 30.1);
persons[3] = new Teacher("scott", 30, 20000);
persons[4] = new Teacher("king", 50, 25000);
// 遍历数组
for (int i = 0; i < persons.length; i++) {
System.out.println(persons[i].say()); // 动态绑定,调用子类重写的say()
// 调用子类特有方法(向下转型)
if (persons[i] instanceof Student) {
((Student) persons[i]).study();
} else if (persons[i] instanceof Teacher) {
((Teacher) persons[i]).teach();
}
}
}
}2) 多态参数
- 方法形参类型为父类类型,实参可传入子类对象
- 统一处理不同子类对象,提高代码复用性
示例:
package com.hspedu.poly_.polyparameter_;
// 父类:Employee
public class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
// 计算年工资
public double getAnnual() {
return 12 * salary;
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getSalary() { return salary; }
public void setSalary(double salary) { this.salary = salary; }
}
// 子类:Manager
public class Manager extends Employee {
private double bonus; // 奖金
public Manager(String name, double salary, double bonus) {
super(name, salary);
this.bonus = bonus;
}
// 特有方法
public void manage() {
System.out.println("经理" + getName() + " is managing");
}
// 重写年工资计算(工资+奖金)
@Override
public double getAnnual() {
return super.getAnnual() + bonus;
}
}
// 子类:Worker
public class Worker extends Employee {
public Worker(String name, double salary) {
super(name, salary);
}
// 特有方法
public void work() {
System.out.println("普通员工" + getName() + " is working");
}
// 年工资直接复用父类方法
@Override
public double getAnnual() {
return super.getAnnual();
}
}
// 测试类
public class PloyParameter {
public static void main(String[] args) {
Worker tom = new Worker("tom", 2500);
Manager milan = new Manager("milan", 5000, 200000);
PloyParameter pp = new PloyParameter();
pp.showEmpAnnual(tom); // 年工资:30000.0
pp.showEmpAnnual(milan); // 年工资:260000.0
pp.testWork(tom); // 普通员工tom is working
pp.testWork(milan); // 经理milan is managing
}
// 多态参数:接收Employee及其子类对象
public void showEmpAnnual(Employee e) {
System.out.println("年工资:" + e.getAnnual());
}
// 调用子类特有方法
public void testWork(Employee e) {
if (e instanceof Worker) {
((Worker) e).work();
} else if (e instanceof Manager) {
((Manager) e).manage();
} else {
System.out.println("不做处理...");
}
}
}Object
equals 方法
== 和 equals 的对比(面试题)
| 特性 | == | equals |
|---|---|---|
| 适用类型 | 基本类型、引用类型 | 仅引用类型 |
| 基本类型判断 | 判断值是否相等 | 不支持 |
| 引用类型判断 | 判断地址是否相等(是否同一对象) | 默认判断地址相等,子类可重写为判断内容相等 |
equals 源码分析
- Object 类的 equals 方法(默认):java
public boolean equals(Object obj) { return (this == obj); // 判断地址是否相等 } - String 类重写 equals 方法(判断内容相等):java
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
示例:
package com.hspedu.object_;
public class Equals01 {
public static void main(String[] args) {
// 基本类型 == 判断值相等
int num1 = 10;
double num2 = 10.0;
System.out.println(num1 == num2); // true
// 引用类型 == 判断地址相等
String str1 = new String("hspedu");
String str2 = new String("hspedu");
System.out.println(str1 == str2); // false(不同对象,地址不同)
System.out.println(str1.equals(str2)); // true(内容相同)
// Integer重写equals方法
Integer i1 = new Integer(1000);
Integer i2 = new Integer(1000);
System.out.println(i1 == i2); // false
System.out.println(i1.equals(i2)); // true
}
}如何重写 equals 方法
需求:判断两个 Person 对象的 name、age、gender 是否完全相同,若相同返回 true。
package com.hspedu.object_;
public class EqualsExercise01 {
public static void main(String[] args) {
Person person1 = new Person("jack", 10, '男');
Person person2 = new Person("jack", 10, '男');
Person person3 = new Person("tom", 20, '女');
System.out.println(person1.equals(person2)); // true
System.out.println(person1.equals(person3)); // false
}
}
class Person {
private String name;
private int age;
private char gender;
public Person(String name, int age, char gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// 重写equals方法
@Override
public boolean equals(Object obj) {
// 1. 判断是否是同一个对象
if (this == obj) {
return true;
}
// 2. 判断obj是否是Person类型
if (obj instanceof Person) {
// 3. 向下转型,获取obj的属性
Person p = (Person) obj;
// 4. 比较属性(字符串用equals,基本类型用==)
return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
}
// 5. 不是Person类型,返回false
return false;
}
}课堂练习题
练习 1
package com.hspedu.object_;
public class EqualsExercise02 {
public static void main(String[] args) {
Person_ p1 = new Person_();
p1.name = "hspedu";
Person_ p2 = new Person_();
p2.name = "hspedu";
System.out.println(p1 == p2); // false(不同对象,地址不同)
System.out.println(p1.name.equals(p2.name)); // true(字符串内容相同)
System.out.println(p1.equals(p2)); // false(未重写equals,默认判断地址)
String s1 = new String("asdf");
String s2 = new String("asdf");
System.out.println(s1.equals(s2)); // true(String重写了equals)
System.out.println(s1 == s2); // false(地址不同)
}
}
class Person_ {
public String name;
}练习 2
int it = 65;
float fl = 65.0f;
System.out.println("65 和65.0f 是否相等?" + (it == fl)); // true(基本类型值相等)hashCode 方法
hashCode 方法定义
public int hashCode()返回该对象的哈希值。支持此方法是为了提高哈希表(例如 java.util.Hashtable)的性能。
hashCode 的常规协定
- 在 Java 应用程序执行期间,对同一对象多次调用
hashCode方法时,必须一致地返回相同的整数,前提是将对象用于equals比较的信息没有被修改。从应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。 - 如果根据
equals(Object)方法,两个对象是相等的,那么对这两个对象中的每个对象调用hashCode方法都必须生成相同的整数结果。 - 如果根据
equals(java.lang.Object)方法,两个对象不相等,那么对这两个对象中的任一对象调用hashCode方法不要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
返回值
此对象的一个哈希码值。
老韩的 6 个小结
- 提高具有哈希结构的容器的效率!
- 两个引用,如果指向的是同一个对象,则哈希值肯定是一样的!
- 两个引用,如果指向的是不同对象,则哈希值是不一样的(非绝对,但推荐)
- 哈希值主要根据地址号生成,不能完全将哈希值等价于地址。
- 案例演示
HashCode_.java:obj.hashCode()(测试:A obj1 = new A(); A obj2 = new A(); A obj3 = obj1) - 后面在集合中,
hashCode如需使用会重写,讲解集合时将说明如何重写hashCode()代码。
案例代码(HashCode_.java)
package com.hspedu.object_;
public class HashCode_ {
public static void main(String[] args) {
AA aa = new AA();
AA aa2 = new AA();
AA aa3 = aa;
System.out.println("aa.hashCode()=" + aa.hashCode());
System.out.println("aa2.hashCode()=" + aa2.hashCode());
System.out.println("aa3.hashCode()=" + aa3.hashCode());
}
}
class AA {}toString 方法
1) 基本介绍
- 默认返回:全类名 +
@+ 哈希值的十六进制(查看Object的toString方法) - 子类往往重写
toString方法,用于返回对象的属性信息
2) 核心特性
- 重写
toString方法后,打印对象或拼接对象时,会自动调用该对象的toString形式 - 当直接输出一个对象时,
toString方法会被默认调用(例如System.out.println(monster);等价于monster.toString())
3) Object 的 toString() 源码解析
public String toString() {
// (1) getClass().getName():类的全类名(包名+类名)
// (2) Integer.toHexString(hashCode()):将对象的 hashCode 转为16进制字符串
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}案例代码(ToString_.java)
package com.hspedu.object_;
public class ToString_ {
public static void main(String[] args) {
Monster monster = new Monster("小妖怪", "巡山的", 1000);
System.out.println(monster.toString() + " hashcode=" + monster.hashCode());
System.out.println("==当直接输出一个对象时,toString 方法会被默认的调用==");
System.out.println(monster); // 等价于 monster.toString()
}
}
class Monster {
private String name;
private String job;
private double sal;
public Monster(String name, String job, double sal) {
this.name = name;
this.job = job;
this.sal = sal;
}
// 重写 toString 方法, 输出对象的属性(快捷键:alt+insert -> toString)
@Override
public String toString() {
return "Monster{" +
"name='" + name + '\'' +
", job='" + job + '\'' +
", sal=" + sal +
'}';
}
@Override
protected void finalize() throws Throwable {
System.out.println("fin..");
}
}finalize 方法
1) 基本作用
当对象被回收时,系统自动调用该对象的 finalize 方法。子类可以重写该方法,做一些释放资源的操作(如关闭数据库连接、文件流等)。
2) 调用时机
当某个对象没有任何引用时,JVM 会将其标记为垃圾对象,随后垃圾回收机制(GC)会销毁该对象。在销毁对象前,会先调用 finalize 方法。
3) 垃圾回收机制说明
- GC 的调用由系统决定(遵循自身算法)
- 可通过
System.gc()主动触发垃圾回收机制
老韩提示
实际开发中几乎不会使用 finalize,更多用于应付面试。
案例代码(Finalize_.java)
package com.hspedu.object_;
// 演示 Finalize 的用法
public class Finalize_ {
public static void main(String[] args) {
Car bmw = new Car("宝马");
// 将对象置为垃圾对象
bmw = null;
System.gc(); // 主动调用垃圾回收器
System.out.println("....");
}
}
class Car {
private String name; // 属性(资源)
public Car(String name) {
this.name = name;
}
// 重写 finalize
@Override
protected void finalize() throws Throwable {
System.out.println("我们销毁汽车" + name);
System.out.println("释放了某些资源...");
}
}断点调试(debug)
实际需求
- 开发中新手查找错误时,可通过断点调试逐步查看源码执行过程,定位错误
- 重要提示:断点调试是运行状态,以对象的运行类型执行(例:
A extends B; B b = new A(); b.xx();按 A 类逻辑执行)
断点调试介绍
- 断点调试:在程序某一行设置断点,运行到该行时暂停,可逐步调试
- 核心作用:
- 查看变量实时值
- 定位错误代码行
- 学习 Java 底层源码执行过程
- 必备技能:程序员必须掌握
断点调试的快捷键
| 快捷键 | 功能描述 |
|---|---|
| F7 | 跳入方法内 |
| F8 | 逐行执行代码 |
| Shift+F8 | 跳出方法 |
| F9 (resume) | 执行到下一个断点 |
断点调试应用案例
案例 1:逐行执行代码(Debug01.java)
package com.hspedu.debug_;
public class Debug01 {
public static void main(String[] args) {
// 演示逐行执行代码
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += i;
System.out.println("i=" + i);
System.out.println("sum=" + sum);
}
}
}案例 2:数组越界异常调试(Debug02.java)
package com.hspedu.debug_;
public class Debug02 {
public static void main(String[] args) {
int[] arr = {1, 10, -1};
// 数组长度为3,i<=3 会导致越界
for (int i = 0; i <= arr.length; i++) {
System.out.println(arr[i]);
}
System.out.println("退出 for");
}
}案例 3:查看 Arrays.sort 底层实现(Debug03.java)
package com.hspedu.debug_;
import java.util.Arrays;
public class Debug03 {
public static void main(String[] args) {
int[] arr = {1, -1, 10, -20, 100};
// 调试查看 Arrays.sort 底层实现
Arrays.sort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
}
}案例 4:动态断点与 F9 用法(Debug04.java)
package com.hspedu.debug_;
import java.util.Arrays;
// 演示执行到下一个断点,支持动态下断点
public class Debug04 {
public static void main(String[] args) {
int[] arr = {1, -1, 10, -20, 100};
Arrays.sort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println("hello100");
System.out.println("hello200");
System.out.println("hello300");
System.out.println("hello400");
System.out.println("hello500");
System.out.println("hello600");
System.out.println("hello700");
}
}断点调试课后练习
- 使用断点调试追踪对象创建过程(
Person[name,age,构造器..]) - 查看动态绑定机制的工作原理
项目-零钱通
项目开发流程说明
化繁为简,分步实现:
- 显示菜单并支持选择
- 零钱通明细
- 收益入账
- 消费
- 退出
项目需求说明
使用 Java 开发零钱通项目,支持功能:
- 收益入账
- 消费
- 查看明细
- 退出系统
项目界面
--零钱通菜单--
1 零钱通明细
2 收益入账
3 消费
4 退 出
请选择(1-4):1
--零钱通明细---
收益入账+100.0 2021-01-21 17:41 余额:100.0
收益入账+500.0 2021-01-21 17:41 余额:600.0
真功夫-20.0 2021-01-21 17:41 余额:580.0项目代码实现(过程编程)
文件:SmallChangeSys.java
项目代码实现改进
- 退出确认:输入 4 退出时,提示"你确定要退出吗?y/n",需输入正确的 y/n
- 金额校验:收益入账和消费时,判断金额是否合理并提示
- 面向对象重构:
- 编写
SmallChangeSysOOP.java类(封装功能) - 编写
SmallChangeSysApp.java完成测试(main 方法)
- 编写
SmallChangeSysOOP 功能清单
- 显示菜单并选择
- 零钱通明细
- 收益入账
- 消费
- 退出
SmallChangeSysApp 测试代码框架
public class SmallChangeSysApp {
public static void main(String[] args) {
// 创建 SmallChangeSysOOP 对象,调用相关方法完成功能
SmallChangeSysOOP smallChangeSys = new SmallChangeSysOOP();
smallChangeSys.mainMenu();
}
}本章作业
Homework01.java
定义 Person 类(name,age,job),初始化 3 个 Person 对象的数组,按 age 从大到小排序(使用冒泡排序)。
Homework02.java
写出四种访问修饰符和各自的访问权限。
Homework03.java
- 编写老师类
Teacher,属性:姓名name、年龄age、职称post、基本工资salary - 业务方法
introduce():输出教师信息 - 编写三个子类:
Professor(教授):工资级别 1.3AssociateProfessor(副教授):工资级别 1.2Lecturer(讲师):工资级别 1.1
- 子类重写
introduce()方法 - 初始化老师对象,调用
introduce()打印信息
Homework04.java
通过继承实现员工工资核算打印功能:
- 父类:
Employee(员工类) - 子类:
Manager(部门经理)、Worker(普通员工) - 工资计算规则:
- 部门经理工资 = 1000 + 单日工资 × 天数 × 1.2(奖金+基本工资)
- 普通员工工资 = 单日工资 × 天数 × 1.0(基本工资)
- 员工属性:姓名、单日工资、工作天数
- 员工方法:打印工资(子类重写)
- 初始化对象并调用打印工资方法
Homework05.java(包:com.hspedu.homework.homework5)
设计继承体系:
- 父类:
Employee(员工类) - 子类:
Worker(工人)、Peasant(农民)、Teacher(教师)、Scientist(科学家)、Waiter(服务生) - 工资规则:
- 工人、农民、服务生:只有基本工资
sal - 教师:基本工资 + 课酬(
classDay天 ×classSal元/天) - 科学家:基本工资 + 年终奖
bonus
- 工人、农民、服务生:只有基本工资
- 测试类:打印各类员工的全年工资
Homework06.java
已知 Grand、Father、Son 在同一个包,分析父类和子类中通过 this 和 super 可调用的属性和方法:
class Grand { // 超类
String name = "AA";
private int age = 100;
public void g1() {}
}
class Father extends Grand { // 父类
String id = "001";
private double score;
public void f1() {}
}
class Son extends Father { // 子类
String name = "BB";
public void g1() {}
private void show() {
// super 可访问哪些成员?
// this 可访问哪些成员?
}
}Homework07.java
写出程序运行结果:
class Test { // 父类
String name = "Rose";
Test() {
System.out.println("Test");
}
Test(String name) {
this.name = name;
}
public void test() {
System.out.println(super.name);
System.out.println(this.name);
}
}
class Demo extends Test { // 子类
String name = "Jack";
Demo() {
super();
System.out.println("Demo");
}
Demo(String s) {
super(s);
}
public static void main(String[] args) {
new Demo().test(); // 匿名对象
new Demo("john").test(); // 匿名对象
}
}Homework08.java
扩展 BankAccount 类:
class BankAccount {
private double balance; // 余额
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
// 存款
public void deposit(double amount) {
balance += amount;
}
// 取款
public void withdraw(double amount) {
balance -= amount;
}
// set 和 getBalance 方法...
}- 扩展
CheckingAccount类:每次存款和取款收取 1 美元手续费 - 扩展
SavingsAccount类:- 每月产生利息(调用
earnMonthlyInterest方法) - 每月 3 次免手续费的存款/取款
earnMonthlyInterest方法中重置交易计数
- 每月产生利息(调用
- 体会重写的好处
Homework09.java
设计类:
Point类:x和y坐标(构造器提供)- 子类
LabeledPoint:构造器接受标签值和x,y坐标(例:new LabeledPoint("Black",1929,230.07))
Homework10.java
编写 Doctor 类(name,age,job,gender,sal):
- 提供 getter/setter 方法
- 5 个参数的构造器
- 重写
Object类的equals方法:判断属性是否完全相同 - 测试类中创建两个对象,判断是否相等
Homework11.java
写出对象向上转型和向下转型的代码,及可调用的方法:
class Person { // 父类
public void run() {
System.out.println("person run");
}
public void eat() {
System.out.println("person eat");
}
}
class Student extends Person { // 子类
public void study() {
System.out.println("student study.");
}
@Override
public void run() {
System.out.println("student run");
}
}Homework12.java
简述 == 和 equals 的区别:
| 名称 | 概念 | 用于基本数据类型 | 用于引用类型 |
|---|---|---|---|
== | 比较运算符 | 可以,判断值相等 | 可以,判断对象地址是否相等 |
equals | Object 类的方法 | 不可以 | 默认判断地址相等;子类常重写为判断属性相等(如 String、Integer) |
Homework13.java
按要求实现类并打印效果:
打印效果
老师的信息:
姓名:张飞
年龄:30
性别:男
工龄:5
我承诺,我会认真教课。
张飞爱玩象棋
学生的信息:
姓名:小明
年龄:15
性别:男
学号:00023102
我承诺,我会好好学习。
小明爱玩足球。需求
Student类:name,sex,age,stu_id,封装,构造器赋值Teacher类:name,sex,age,work_age,封装,构造器赋值- 抽取父类
Person,封装共同属性和方法 Student有study()方法:输出"我承诺,我会好好学习。"Teacher有teach()方法:输出"我承诺,我会认真教学。"- 父子类共有
play()方法(父类定义,子类重写):- 学生返回"xx 爱玩足球"
- 老师返回"xx 爱玩象棋"
- 定义多态数组(2 个学生+2 个教师),按年龄从高到低排序
- 定义方法(形参为
Person类型):调用学生的study()或教师的teach()
Homework14.java
程序阅读题:执行 C c = new C(); 输出什么?
class A { // 超类
public A() {
System.out.println("我是A类");
}
}
class B extends A { // 父类
public B() {
System.out.println("我是B类的无参构造");
}
public B(String name) {
System.out.println(name + "我是B类的有参构造");
}
}
class C extends B { // 子类
public C() {
this("hello");
System.out.println("我是c类的无参构造");
}
public C(String name) {
super("hahah");
System.out.println("我是c类的有参构造");
}
}Homework15.java
- 什么是多态?
- 多态的具体体现(举例说明):
- 方法多态(重载、重写)
- 对象多态(编译类型与运行类型不一致)
Homework16.java
什么是 Java 的动态绑定机制?
- 调用对象方法时,方法与对象的内存地址/运行类型绑定
- 调用对象属性时,无动态绑定机制,哪里声明哪里使用